package de.jmda.core.util.jpa;

import java.lang.annotation.Annotation;
import java.util.List;

import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;

import org.apache.log4j.Logger;

import de.jmda.core.mproc.ProcessingUtilities;

/**
 * Abstract utility class that provides methods to derive {@link
 * EntityRelationInfo} objects from a single declared field.
 */
public abstract class EntityRelationInfoFactory
{
	private final static Logger LOGGER =
			Logger.getLogger(EntityRelationInfoFactory.class);

	public static EntityRelationInfo getEntityRelationInfo(
			VariableElement declaredField)
	{
		Annotation annotation = declaredField.getAnnotation(OneToOne.class);

		if (annotation != null)
		{
			return getEntityRelationInfo(declaredField, (OneToOne) annotation);
		}

		annotation = declaredField.getAnnotation(OneToMany.class);

		if (annotation != null)
		{
			return getEntityRelationInfo(declaredField, (OneToMany) annotation);
		}

		annotation = declaredField.getAnnotation(ManyToOne.class);

		if (annotation != null)
		{
			return getEntityRelationInfo(declaredField, (ManyToOne) annotation);
		}

		annotation = declaredField.getAnnotation(ManyToMany.class);

		if (annotation != null)
		{
			return getEntityRelationInfo(declaredField, (ManyToMany) annotation);
		}

		return null;
	}

	/**
	 * @param declaredField
	 * @param oneToOne annotation from <code>declaredField</code>
	 * @return entity relation info
	 */
	public static EntityRelationInfo getEntityRelationInfo(
			VariableElement declaredField, OneToOne oneToOne)
	{
		String mappedByValue = oneToOne.mappedBy();

		VariableElement owningField;
		VariableElement inverseField;

		if (mappedByValue.equals(""))
		{
			// If there is no mappedByValue the relation is either unidirectional or
			// the declaredField corresponding to fieldDeclaration owns the bidirectional
			// relation.

			// Try to find an inverse declaredField.
			inverseField = getInverseField(declaredField, oneToOne);

			if (inverseField == null)
			{
				// No inverse declaredField declaration found, so the relation is unidirectional.
				return new EntityRelationInfoOneToOneUnidirectional(declaredField);
			}
			else
			{
				// Inverse declaredField declaration found, so the relation is bidirectional.
				return
						new EntityRelationInfoOneToOneBidirectional(
								declaredField, declaredField, inverseField);
			}
		}
		else
		{
			// If there is a mappedByValue the relation is bidirectional and the declaredField
			// corresponding to fieldDeclaration itself is an inverse declaredField. Now the
			// owning declaredField in the inverse's type must be found.
			inverseField = declaredField;

			owningField = getOwningField(inverseField, oneToOne);

			if (owningField != null)
			{
				return
						new EntityRelationInfoOneToOneBidirectional(
								declaredField, owningField, inverseField);
			}
			else
			{
				String errMsg =
						"incomplete bidirectional one to one relation, no owning declaredField " +
								"for inverse declaredField [" + inverseField.getSimpleName() +
								"] with mapped by value [" + mappedByValue + "]";
				LOGGER.error(errMsg);
				throw new IllegalStateException(errMsg);
			}
		}
	}

	/**
	 * @param owningField
	 * @param oneToOne annotation from <code>owningField</code>
	 * @return inverse declaredField or <code>null</code> if unable to find one
	 */
	public static VariableElement getInverseField(
			VariableElement owningField, OneToOne oneToOne)
	{
		TypeElement owningClass =
				ProcessingUtilities.asTypeElement(owningField.getEnclosingElement());

		if (ProcessingUtilities.isClassKind(owningClass) == false)
		{
			LOGGER.warn(
					"expected enclosing element to have kind [" + ElementKind.CLASS +
					"], found [" + owningClass.asType() + "]");
			return null;
		}

		TypeElement inverseClass =
				ProcessingUtilities.asTypeElement(
						ProcessingUtilities.asDeclaredType(owningField.asType()).asElement());

		// fetch the fields of inverse class
		List<VariableElement> inverseFieldCandidates =
				ElementFilter.fieldsIn(inverseClass.getEnclosedElements());

		// check all inverse variable declaredField candidates
		for (VariableElement inverseFieldCandidate : inverseFieldCandidates)
		{
			// check if inverse declaredField candidate declaration is annotated with OneToOne
			OneToOne inverseFieldCandidateOneToOne =
					inverseFieldCandidate.getAnnotation(OneToOne.class);

			if (inverseFieldCandidateOneToOne != null)
			{
				// check if inverse declaredField candidate's mapped by value matches owning
				// declaredField's simple name AND if the inverse declaredField candidate's type
				// corresponds to the ownings declaredField's type
				if (inverseFieldCandidateOneToOne.mappedBy().equals(
						owningField.getSimpleName().toString()) &&
						inverseFieldCandidate.asType().equals(
								owningClass.asType()))
				{
					return inverseFieldCandidate;
				}
			}
		}

		return null;
	}

	/**
	 * @param inverseField
	 * @param oneToOne annotation from <code>inverseFieldCandidate</code>
	 * @return owning declaredField or <code>null</code> if unable to find one
	 */
	public static VariableElement getOwningField(
			VariableElement inverseField, OneToOne oneToOne)
	{
		TypeMirror inverseFieldType = inverseField.asType();

		if (ProcessingUtilities.isDeclaredKind(inverseFieldType) == false)
		{
			LOGGER.warn(
					"expected [" + TypeKind.DECLARED + "] for inverseField," +
							"found [" + inverseField.asType().getKind() + "]");
			return null;
		}

		TypeElement inverseFieldTypeElement =
				ProcessingUtilities.asTypeElement(inverseFieldType);

		// fetch the fields of inverse field's type (target class)
		List<VariableElement> owningFieldCandidates =
				ElementFilter.fieldsIn(inverseFieldTypeElement.getEnclosedElements());

		String mappedByValue = oneToOne.mappedBy();

		// check all owning declaredField declaration candidates
		for (VariableElement owningFieldCandidate : owningFieldCandidates)
		{
			// check if owning declaredField candidate declaration is annotated with
			// OneToOne
			OneToOne owningFieldCandidateOneToOne =
					owningFieldCandidate.getAnnotation(OneToOne.class);

			if (owningFieldCandidateOneToOne != null)
			{
				// check if owning declaredField candidate's simple name matches
				// mapped by value
				if (owningFieldCandidate.getSimpleName().toString().equals(
						mappedByValue))
				{
					return owningFieldCandidate;
				}
			}
		}

		return null;
	}

	public static EntityRelationInfo getEntityRelationInfo(
			VariableElement declaredField, OneToMany oneToMany)
	{
		String mappedByValue = oneToMany.mappedBy();

		VariableElement owningField;
		VariableElement inverseField;

		if (mappedByValue.equals(""))
		{
			// No inverse declaredField declaration found, so the relation is unidirectional.
			return new EntityRelationInfoOneToManyUnidirectional(declaredField);
		}
		else
		{
			// If there is a mappedByValue the relation is bidirectional.
			// Assume that fieldDeclaration itself is an inverse declaredField (this is true
			// for all relations that have different classes at both sides of the
			// relation).
			inverseField = declaredField;

			// Now the owning declaredField in the inverse's type must be found.
			owningField = getOwningField(inverseField, oneToMany);

			if (owningField != null)
			{
				if (inverseField.asType().equals(owningField.asType()))
				{
					// if relation has the same class at both sides of the relation the
					// declaredField with the mappedBy value (fieldDeclaration) is the owning
					// declaredField
					// hence swap the declaredField declarations
					VariableElement buffer = owningField;
					owningField = inverseField;
					inverseField = buffer;
				}
				return
						new EntityRelationInfoOneToManyBidirectional(
								declaredField, owningField, inverseField);
			}
			else
			{
				String errMsg =
						"incomplete bidirectional one to many relation, no owning declaredField " +
								"for inverse declaredField [" + inverseField.asType() + "." +
								inverseField.getSimpleName() + "] with mapped by value [" +
								mappedByValue + "]";
				LOGGER.error(errMsg);
				throw new IllegalStateException(errMsg);
			}
		}
	}

	/**
	 * @param inverseField
	 * @param oneToMany annotation from <code>fieldDeclaration</code>
	 * @return owning declaredField or <code>null</code> if unable to find one
	 */
	public static VariableElement getOwningField(
			VariableElement inverseField, OneToMany oneToMany)
	{
		TypeElement inverseClass =
				ProcessingUtilities.asTypeElement(inverseField.getEnclosingElement());

		if (ProcessingUtilities.isClassKind(inverseClass) == false)
		{
			LOGGER.warn(
					"expected enclosing element to have kind [" + ElementKind.CLASS +
					"], found [" + inverseClass.asType() + "]");
			return null;
		}

		DeclaredType inverseFieldType =
				ProcessingUtilities.asDeclaredType(inverseField.asType());

		// try to get inverseFieldCandidateType's type argument
		List<? extends TypeMirror> inverseFieldTypeArguments =
				inverseFieldType.getTypeArguments();

		if (inverseFieldTypeArguments.size() != 1)
		{
			LOGGER.warn(
					"expected exactly 1 type argument, " +
							"found " + inverseFieldTypeArguments.size());
			return null;
		}

		TypeMirror inverseFieldTypeArgument =
				inverseFieldTypeArguments.iterator().next();

		if (ProcessingUtilities.isDeclaredKind(inverseFieldTypeArgument) == false)
		{
			LOGGER.warn(
					"expected [" + TypeKind.DECLARED + "] instance," +
							"found [" + inverseFieldTypeArgument + "]");
			return null;
		}

		// check all owning declaredField candidates
		List<VariableElement> owningFieldCandidates =
				ElementFilter.fieldsIn(
						ProcessingUtilities.asDeclaredType(
								inverseFieldTypeArgument).asElement().getEnclosedElements());

		for (VariableElement owningFieldCandidate : owningFieldCandidates)
		{
			// check if owning declaredField candidate declaration is annotated with
			// ManyToOne
			ManyToOne owningFieldCandidateManyToOne =
					owningFieldCandidate.getAnnotation(ManyToOne.class);

			if (owningFieldCandidateManyToOne != null)
			{
				// check if owning declaredField candidate's simple name matches inverse
				// declaredField's mapped by value
				if (owningFieldCandidate.getSimpleName().toString().equals(
						oneToMany.mappedBy()))
				{
					return owningFieldCandidate;
				}
			}
		}

		return null;
	}

	public static EntityRelationInfo getEntityRelationInfo(
			VariableElement declaredField, ManyToOne manyToOne)
	{
		VariableElement inverseField = getInverseField(declaredField, manyToOne);

		if (inverseField != null)
		{
			return
					new EntityRelationInfoManyToOneBidirectional(
							declaredField, declaredField, inverseField);
		}

		return new EntityRelationInfoManyToOneUnidirectional(declaredField);
	}

	/**
	 * @param owningField
	 * @param manyToOne annotation from <code>owningField</code>
	 * @return inverse declaredField or <code>null</code> if unable to find one
	 */
	public static VariableElement getInverseField(
			VariableElement owningField, ManyToOne manyToOne)
	{
		TypeElement owningClass =
				ProcessingUtilities.asTypeElement(owningField.getEnclosingElement());

		if (ProcessingUtilities.isClassKind(owningClass) == false)
		{
			LOGGER.warn(
					"expected enclosing element to have kind [" + ElementKind.CLASS +
					"], found [" + owningClass.asType() + "]");
			return null;
		}

		TypeElement inverseClass =
				ProcessingUtilities.asTypeElement(
						ProcessingUtilities.asDeclaredType(owningField.asType()).asElement());

		// check all inverse declaredField declaration candidates
		List<VariableElement> inverseFieldCandidates =
				ElementFilter.fieldsIn(inverseClass.getEnclosedElements());

		// check all inverse declaredField candidates
		for (VariableElement inverseFieldCandidate : inverseFieldCandidates)
		{
			// check if inverse declaredField candidate declaration is annotated with
			// OneToMany
			OneToMany oneToMany = inverseFieldCandidate.getAnnotation(OneToMany.class);

			if (oneToMany != null)
			{
				DeclaredType inverseFieldCandidateType =
						ProcessingUtilities.asDeclaredType(inverseFieldCandidate.asType());

				// try to get inverseFieldCandidateType's type argument
				List<? extends TypeMirror> typeArguments =
						inverseFieldCandidateType.getTypeArguments();

				if (typeArguments.size() == 1)
				{
					TypeMirror typeArgument = typeArguments.iterator().next();

					// check if
					//   1. inverse declaredField candidate's mapped by value matches owning
					//      declaredField's simple name AND if
					//   2. the inverse declaredField candidate's type parameter class corresponds
					//      to the owning declaredField's declaring type

					String owningFieldClassName =
							owningField.getEnclosingElement().asType().toString();

					if (oneToMany.mappedBy().equals(
							owningField.getSimpleName().toString()) &&
							typeArgument.toString().equals(owningFieldClassName))
					{
						return inverseFieldCandidate;
					}
				}
				else if (typeArguments.size() == 0)
				{
					// the inverse declaredField candidate has no type parameter
					// this may be the case if no generics were used
					// return the first inverse declaredField candidate with matching mapped by
					// value
					if (oneToMany.mappedBy().equals(
							owningField.getSimpleName().toString()))
					{
						LOGGER.warn(
								"use generic type parameters for declaredField [" +
										inverseFieldCandidate.getEnclosingElement()
										.asType().toString() + "." +
										inverseFieldCandidate.getSimpleName() + "] to " +
										"reduce possible ambiguity for mapping owning and inverse " +
								"declaredField");
						return inverseFieldCandidate;
					}
				}
				else
				{
					String errMsg =
							"unexpected number of type arguments, expected [0] or [1], " +
									"found [" + typeArguments.size() + "]";
					LOGGER.error(errMsg);
					throw new IllegalStateException(errMsg);
				}
			}
		}

		return null;
	}

	public static EntityRelationInfo getEntityRelationInfo(
			VariableElement declaredField, ManyToMany manyToMany)
	{
		String mappedByValue = manyToMany.mappedBy();
		VariableElement owningField;
		VariableElement inverseField;

		if (mappedByValue.equals(""))
		{
			// If there is no mappedByValue the relation is either unidirectional or
			// the declaredField corresponding to fieldDeclaration owns the bidirectional
			// relation.

			// Try to find an inverse declaredField declaration.
			inverseField = getInverseField(declaredField, manyToMany);

			if (inverseField == null)
			{
				// No inverse declaredField declaration found, so the relation is unidirectional.
				return
						new EntityRelationInfoManyToManyUnidirectional(
								declaredField);
			}
			else
			{
				// Inverse declaredField declaration found, so the relation is bidirectional.
				return
						new EntityRelationInfoManyToManyBidirectional(
								declaredField, declaredField, inverseField);
			}
		}
		else
		{
			// If there is a mappedByValue the relation is bidirectional and the declaredField
			// corresponding to fieldDeclaration itself is an inverse declaredField. Now the
			// owning declaredField in the inverse's type must be found.

			inverseField = declaredField;
			owningField = getOwningField(inverseField, manyToMany);

			if (owningField != null)
			{
				return
						new EntityRelationInfoManyToManyBidirectional(
								declaredField, owningField, inverseField);
			}
			else
			{
				String errMsg =
						"incomplete bidirectional many to many relation, no owning declaredField " +
								"for inverse declaredField [" + inverseField.getSimpleName() +
								"] with mapped by value [" + mappedByValue + "]";
				LOGGER.error(errMsg);
				throw new IllegalStateException(errMsg);
			}
		}
	}

	/**
	 * @param owningField
	 * @param manyToMany annotation from <code>fieldDeclaration</code>
	 * @return inverse declaredField or <code>null</code> if unable to find one
	 */
	public static VariableElement getInverseField(
			VariableElement owningField, ManyToMany manyToMany)
	{
		TypeElement owningClass =
				ProcessingUtilities.asTypeElement(owningField.getEnclosingElement());

		if (ProcessingUtilities.isClassKind(owningClass) == false)
		{
			LOGGER.warn(
					"expected enclosing element to have kind [" + ElementKind.CLASS +
					"], found [" + owningClass.asType() + "]");
			return null;
		}

		DeclaredType owningFieldType =
				ProcessingUtilities.asDeclaredType(owningField.asType());

		// try to get inverseFieldCandidateType's type argument
		List<? extends TypeMirror> owningFieldTypeArguments =
				owningFieldType.getTypeArguments();

		if (owningFieldTypeArguments.size() != 1)
		{
			LOGGER.warn(
					"expected exactly 1 type argument, " +
							"found " + owningFieldTypeArguments.size());
			return null;
		}

		TypeMirror owningFieldTypeArgument =
				owningFieldTypeArguments.iterator().next();

		if (ProcessingUtilities.isDeclaredKind(owningFieldTypeArgument) == false)
		{
			LOGGER.warn(
					"expected [" + TypeKind.DECLARED + "] instance," +
							"found [" + owningFieldTypeArgument + "]");
			return null;
		}

		// check all inverse declaredField candidates
		List<VariableElement> inverseFieldCandidates =
				ElementFilter.fieldsIn(
						ProcessingUtilities.asDeclaredType(
								owningFieldTypeArgument).asElement().getEnclosedElements());

		for (VariableElement inverseFieldCandidate : inverseFieldCandidates)
		{
			// check if inverse declaredField candidate declaration is annotated with
			// ManyToMany
			ManyToMany manyToManyInverse =
					inverseFieldCandidate.getAnnotation(ManyToMany.class);

			if (manyToManyInverse != null)
			{
				DeclaredType inverseFieldCandidateType =
						ProcessingUtilities.asDeclaredType(inverseFieldCandidate.asType());

				// try to get inverseFieldCandidateType's type argument
				List<? extends TypeMirror> typeArguments =
						inverseFieldCandidateType.getTypeArguments();

				if (typeArguments.size() == 1)
				{
					TypeMirror typeArgument = typeArguments.iterator().next();

					// check if
					//   1. inverse declaredField candidate's mapped by value matches owning
					//      declaredField's simple name AND if
					//   2. the inverse declaredField candidate's type parameter class corresponds
					//      to the owning declaredField's declaring type

					String owningFieldClassName =
							owningField.getEnclosingElement().asType().toString();

					if (manyToManyInverse.mappedBy().equals(
							owningField.getSimpleName().toString()) &&
							typeArgument.toString().equals(owningFieldClassName))
					{
						return inverseFieldCandidate;
					}
				}
				else
				{
					// the inverse declaredField candidate has no type parameter
					// this may be the case if no generics were used
					// return the first inverse declaredField candidate with matching mapped by
					// value
					if (manyToManyInverse.mappedBy().equals(
							owningField.getSimpleName().toString()))
					{
						LOGGER.warn(
								"use generic type parameters for declaredField [" +
										inverseFieldCandidate.getEnclosingElement()
										.asType().toString() + "." +
										inverseFieldCandidate.getSimpleName() + "] to " +
										"reduce possible ambiguity for mapping owning and inverse " +
								"declaredField");
						return inverseFieldCandidate;
					}
				}
			}
		}

		return null;
	}

	/**
	 * @param inverseField
	 * @param manyToMany annotation from <code>fieldDeclaration</code>
	 * @return owning field
	 */
	public static VariableElement getOwningField(
			VariableElement inverseField, ManyToMany manyToMany)
	{
		DeclaredType inverseFieldType =
				ProcessingUtilities.asDeclaredType(inverseField.asType());

		// try to get inverseFieldCandidateType's type argument
		List<? extends TypeMirror> inverseFieldTypeArguments =
				inverseFieldType.getTypeArguments();

		if (inverseFieldTypeArguments.size() != 1)
		{
			LOGGER.warn(
					"expected exactly 1 type argument, " +
							"found " + inverseFieldTypeArguments.size());
			return null;
		}

		TypeMirror inverseFieldTypeArgument =
				inverseFieldTypeArguments.iterator().next();

		if (ProcessingUtilities.isDeclaredKind(inverseFieldTypeArgument) == false)
		{
			LOGGER.warn(
					"expected [" + TypeKind.DECLARED + "] instance," +
							"found [" + inverseFieldTypeArgument + "]");
			return null;
		}

		// check all owning declaredField candidates
		List<VariableElement> owningFieldCandidates =
				ElementFilter.fieldsIn(
						ProcessingUtilities.asDeclaredType(
								inverseFieldTypeArgument).asElement().getEnclosedElements());

		for (VariableElement owningFieldCandidate : owningFieldCandidates)
		{
			// check if owning declaredField candidate declaration is annotated with
			// ManyToMany
			ManyToMany manyToManyOwning =
					owningFieldCandidate.getAnnotation(ManyToMany.class);

			if (manyToManyOwning != null)
			{
				// check if owning declaredField candidate's simple name matches inverse
				// declaredField's mapped by value
				if (owningFieldCandidate.getSimpleName().toString().equals(
						manyToMany.mappedBy()))
				{
					return owningFieldCandidate;
				}
			}
		}

		return null;
	}
}